#!/usr/bin/python
# $Id: $
# 
# RenderfarmHack - Render Job Executor
# Copyright (C) 2010 Adhi Hargo <cadmus_sw at yahoo.com>
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
#  
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#  
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
# USA.


from app import common
from StringIO import StringIO
import os, re, shutil, subprocess, sys, time

def blender254_config(config={}, **kwargs):
    '''Mengubah dictionary konfigurasi menjadi teks script BPython'''
    f = StringIO()
    config.update(kwargs)

    # import required modules
    f.write('import bpy, os, sys\n')
    
    f.write('''
scn = bpy.context.scene
srn = scn.render
scn.render.file_format = "PNG"
''')

    # override configuration
    data = config.get('resPercent')
    if data: f.write('srn.resolution_percentage = %d\n' % data)
    data = config.get('resX')
    if data: f.write('srn.resolution_x = %d\n' % data)
    data = config.get('resY')
    if data: f.write('srn.resolution_y = %d\n' % data)

    f.write('''
resX = scn.render.resolution_x * (srn.resolution_percentage / 100.0)
resY = scn.render.resolution_y * (srn.resolution_percentage / 100.0)
''')

    data = config.get('outputPath')
    if data: f.write('srn.filepath = "%s%%04d%%04d-#####" %% (resX, resY)\n' % data)

    data = config.get('frameStart')
    if data: f.write('scn.frame_start = %d\n' % data)
    data = config.get('frameEnd')
    if data: f.write('scn.frame_end = %d\n' % data)

    # do render operation
    data = config.get('openGL')
    if data:
        f.write('bpy.ops.render.opengl(animation=True, view_context=False)\n')
        f.write('''
if bpy.app.version[:2] < (2, 56):
        bpy.ops.wm.exit_blender()
else:
        bpy.ops.wm.quit_blender()
''')
    else:
        f.write('bpy.ops.render.render(animation=True)\n')

    f.seek(0)
    return f

def process_config(opts):
    '''Mengubah opsi baris perintah menjadi dictionary internal'''
    config = {}
    if opts.resPair:
        match = re.match('(\d+)x(\d+)$', opts.resPair)
        if match:
            config['resX'], config['resY'] = map(int, match.groups())
        else:
            sys.stderr.write('> Cacat spesifikasi resolusi: "%s"\n' % opts.resPair)
            sys.exit(1)

    if opts.resPercent:
        config['resPercent'] = opts.resPercent

    if opts.frames:
        match = re.match('(\d+)(?:\-(\d+)|\+(\d+))?$', opts.frames)
        if match:
            frameStart, frameEnd, frameCount = match.groups()
            frameStart = int(frameStart)
            if frameCount:
                frameEnd = int(frameStart + int(frameCount))
            elif frameEnd:
                frameEnd = int(frameEnd)
                if frameStart > frameEnd:
                    frameStart, frameEnd = frameEnd, frameStart
            else:
                frameEnd = frameStart
            config['frameStart'] = frameStart
            config['frameEnd'] = frameEnd
        else:
            sys.stderr.write('> Cacat spesifikasi frame: "%s"\n' % opts.frames)
            sys.exit(1)

    if opts.opengl:
        config['openGL'] = opts.opengl

    return config

blender25_exe = 'blender'
blender_arg = ' %(bg)s %(blend)s -P %(script)s'

def exec_job(exePath, filePath, outputPath, configDict,
             scriptPath='job.tmp', pollFunc=None):
    '''Melakukan render untuk file FILEPATH berdasarkan konfigurasi
    dalam CONFIGDICT, file2 hasil render diletakkan di
    OUTPUTPATH. OUTPUTPATH dapat turut meliputi pola nama file
    hasil. SCRIPTPATH berisi nama file temporer yang akan menampung
    script render, dan POLLFUNC akan dipanggil dalam durasi tertentu
    selama proses render.'''
    returncode = False

    configObj = blender254_config(configDict, outputPath=outputPath)

    with open(scriptPath, 'wb') as scriptFile:
        shutil.copyfileobj(configObj, scriptFile)

    # Render OpenGL tidak dapat dilakukan lewat eksekusi background
    # karena membutuhkan GUI, maka untuk modus render ini kita
    # aktifkan GUI sekadar agar dapat memanfaatkan OpenGL, lalu
    # segera dimatikan melalui script.
    commandStr = exePath \
        + blender_arg % dict(blend=filePath, script=scriptPath,
                             bg= '-b' if not configDict.get('openGL') else '')

    try:
        proc = subprocess.Popen(commandStr, shell = True,
                                stdout = sys.stdout,
                                stderr = subprocess.PIPE)
        while True:
            try:
                proc.poll()
                if pollFunc != None:
                    pollFunc()
                if proc.returncode != None:
                    # Return code berdasarkan sukses-tidaknya render
                    returncode = proc.returncode == 0
                    break
                time.sleep(2)
            except KeyboardInterrupt:
                sys.stderr.write('> Job interrupted\n')
    finally:
        os.remove(scriptPath)

    return returncode


# ============================= Entry point ============================

def main(opts, args):
    configDict = process_config(opts)
    for filePath in args:
        basename = os.path.basename(filePath)
        if opts.localRepo:
            filePath = os.path.join(opts.localRepo, filePath)

        rootname = os.path.splitext(basename)[0]
        if rootname:
            basename = rootname

        outputPath = basename
        exec_job(opts.exePath, filePath, outputPath, configDict)

def renderArguments(parser):
    '''Argumen baris perintah khusus eksekutor job render'''
    parser.set_defaults(exePath='blender')
    parser.add_option('-B', '--exe', dest='exePath',
                      help='Path program Blender',
                      metavar='PATH')
    parser.add_option('-L', '--repo', dest='localRepo',
                      help='Lokasi file .blend',
                      metavar='PATH')
    parser.add_option('-r', '--res', dest='resPair',
                      help='Resolusi gambar', metavar='WIDTHxHEIGHT')
    parser.add_option('-s', '--scale', dest='resPercent', type='int',
                      help='Ukuran gambar final, '
                      'sebagai persentase skala dari resolusi gambar',
                      metavar='PERCENTSCALE')
    parser.add_option('-f', '--frames', dest='frames',
                      help='Nomor frame yang akan dirender. '
                      'FRAMERANGESPEC dapat berformat "TUNGGAL", '
                      '"AWAL+JUMLAH", atau "AWAL-AKHIR"', metavar='FRAMERANGESPEC')
    parser.add_option('-g', '--opengl', action='store_true', dest='opengl',
                      help='Render OpenGL')
    parser.add_option('-T', '--threads', dest='threads',
                      help='Jumlah thread (~= core CPU) yang dialokasikan untuk '
                      'render (1-64). Hanya berpengaruh untuk prosesor multi-core.')

if __name__ == '__main__':
    opts, args = common.process_args(renderArguments)
    main(opts, args)
